# -*- coding: utf-8 -*-
#coding=utf-8
import flask, os,sys,time,re, urllib.parse
from webPanLib import *
from flask import request, send_from_directory,redirect,url_for,jsonify,make_response,render_template
from flask import session
import shutil
from flask import send_file
from flask import Response
#interface_path = os.path.dirname(__file__)
#sys.path.insert(0, interface_path)  #将当前文件的父目录加入临时系统变量
import re

server = flask.Flask(__name__)
server.secret_key = 'your_secret_key_20250713'  # 必须设置密钥才能使用session

# 配置会话超时时间: 600=10分钟,单位:秒
#SESSION_TIMEOUT = 10  # 10s 测试用
SESSION_TIMEOUT = 60*60*24*2  # 48h



#首页跳转到文件浏览页面
@server.route('/', methods=['get'])
def index():
    return redirect("/list")
#

# 目前，list, delete, upload 需要登录；其他知道具体路径可以直接看文件
# 定义登录装饰器
from functools import wraps
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not session.get('logged_in'):
            # 记录原始请求路径
            return redirect(url_for('login', next=request.url))
        
        # 检查会话是否超时（最后一次活动时间）
        current_time = time.time()
        last_activity = session.get('last_activity', 0)
        # 计算时间差
        time_diff = current_time - last_activity    
        # 如果超过10分钟未活动，会话过期
        if time_diff > SESSION_TIMEOUT:
            session.clear()
            return redirect(url_for('login', next=request.url))
        # 更新最后活动时间为当前时间
        session['last_activity'] = current_time

        # 前面额外的执行完，才执行函数本身
        return f(*args, **kwargs)
    return decorated_function


# 添加before_request钩子来更新活动时间
#@server.before_request
#def update_last_activity():
#    """在每次请求前更新最后活动时间"""
#    if session.get('logged_in'):
#        session['last_activity'] = time.time()



# 登录页
@server.route('/login', methods=['get', 'POST'])
def login():
    if request.method=='GET':
        # 记录原始请求路径
        original_url = request.args.get('next') or url_for('index')
        session['next_url'] = original_url
        if (passwd=="") or ("logged_in" in session and session['logged_in'] == True):
            print("--> no passwd, so login directly.")
            session['logged_in'] = True
            session['last_activity'] = time.time()
            return redirect(original_url)

        return render_template("login.html", version=version, foot=foot0)
    if request.method=='POST':
        # 简单验证示例
        # 如果没有设置密码，则不需要登录; 否则user和passwd都要输入正确
        print("==> passwd=", passwd)
        if (passwd=="") or (request.form['username'] == user and request.form['password'] == passwd):
            session['logged_in'] = True
            session['last_activity'] = time.time()  # 初始化最后活动时间

            # 跳转回原始请求页面
            next_url = session.pop('next_url', url_for('index'))
            return redirect(next_url)
        return '{"msg":"login failed!"}<br><a href="/">Go back home</a>'

# 退出登录
@server.route('/logout')
def logout():
    session.clear()
    return redirect("/login")



#download 指定目录下载文件
# v0.2 怎么区分是否在新窗口打开？
@server.route('/download', methods=['get'])
def download():
    fpath = request.values.get('path', '') #获取文件路径
    fname = request.values.get('filename', '')  #获取文件名
    fpathT = os.path.join(root_path, fpath) #真实路径
    if fname.strip() and fpath.strip():
        #print(fname, fpath);
        if os.path.isfile(os.path.join(fpathT,fname)): # and os.path.isdir(fpath):
            #response = make_response(send_from_directory(fpathT, fname, as_attachment=False))
            #response.headers["Content-Disposition"] = "attachment; filename="+fname.format(fpathT.encode().decode('utf-8'))
            #return response;
            return send_from_directory(fpathT, fname, as_attachment=True) #返回要下载的文件内容给客户端
        else:
            return '{"msg2":"参数不正确"}path=%s, filename=%s;' %(fpathT, fname);
    else:
        return '{"msg1":"请输入参数"}'
#





# 预览图片
@server.route('/show/', methods=['GET'])
def show_photo():
    url = request.values.get('url', '')
    return render_template("show.html", url=url)
#


# 预览代码
@server.route('/code_reader/', methods=['GET'])
def code_reader():
    url = request.values.get('url', '')
    return render_template("code_reader.html", file=url)
#


# 预览 jupyter notebook
@server.route('/nbpreview/', methods=['GET'])
def nbpreview():
    return render_template("nbpreview/index.html")
#


# 预览 markdown
@server.route('/md_reader/', methods=['GET'])
def md_reader():
    return render_template("md_reader/index.html")
#



##############
# 支持跨域访问
# version: 0.2
# version: 0.3 支持pdf预览
# version: 0.4 为了兼容高版本 flask, 去掉 send_file() 第一个参数名
# version: 0.5 支持 Range 头
##############
#路径名字不能是/static/,因为它是内部定义过的静态文件路径
@server.route('/file/<path:filePath>', methods=['GET', 'HEAD'])
def audio(filePath):
    #fpath = request.values.get('path', '') #获取文件路径
    #fname = request.values.get('filename', '')  #获取文件名
    fpathT = os.path.join(root_path, filePath) #真实路径
    print("Real file path: ",fpathT)


    range_header = request.headers.get('Range', None)
    print("0=> Range: ", range_header)
    print("################## headers Begin")
    print(request.headers)
    print("################## headers End")



    if filePath.strip():
        if os.path.isfile(fpathT):
            #blob=''
            #try:
            #    with open( fpathT, 'rb') as file:
            #        blob = file.read()
            #except Exception as e:
            #    print(e)
            #    pass
            
            #res = make_response(blob)
            #res.mimetype='application/octet-stream'
            #res.headers['Access-Control-Allow-Origin'] = '*'
            #return res;

            if not range_header:
                print("\n\n1=> No Range header")
                # 如果没有 Range 头，返回整个文件
                response = make_response(send_file( fpathT, as_attachment=False))
                response.headers["Access-Control-Allow-Origin"]="*"
                response.headers['Accept-Ranges']='bytes'
                return response

            # 解析 Range 头
            print("2=> get partial file with Range")
            file_size = os.path.getsize(fpathT)
            start, end = parse_range_header(range_header, file_size)
            if start >= file_size or end >= file_size:
                return "Invalid range", 416

            # 读取文件的部分内容
            with open(fpathT, 'rb') as f:
                f.seek(start)
                data = f.read(end - start + 1)
            # 构建响应
            response = Response(
                data,
                206,  # 206 Partial Content
                mimetype='application/octet-stream',
                direct_passthrough=True
            )
            response.headers.add('Content-Range', f'bytes {start}-{end}/{file_size}')
            response.headers.add('Accept-Ranges', 'bytes')
            response.headers.add('Content-Length', str(end - start + 1))
            response.headers.add('Access-Control-Allow-Origin', '*')
            return response
        else:
            return '{"msg2":"参数不正确"}path=%s, filename=%s;' %(fpathT, filePath);
    else:
        return '{"msg1":"请输入参数"}'
#ajax test OK: http://127.0.0.1:8005/file/tmp.R


def parse_range_header(range_header, file_size):
    """
    解析 Range 头，返回起始和结束位置
    """
    range_type, range_value = range_header.split('=')
    if range_type.strip() != 'bytes':
        raise ValueError("Unsupported range type")

    start, end = range_value.split('-')
    start = int(start) if start else 0
    end = int(end) if end else file_size - 1

    return start, end

# 测试
# curl -i -H "Range: bytes=0-499" http://j3.biomooc.com:8000/file/dustbin/tmp_links/output_hla/24B74088/star/24B74088.Log.final.out




# 删除文件 ajax方式
# 删除到dustbin文件夹
# v0.3 在配置文件夹中设置不能删除的文件夹
@server.route('/delete', methods=['POST'])
@login_required
def delete():
    fpath = request.form.get('path', '') #获取文件路径
    fnameStr = request.form.get('filenames', '')  #获取文件名
    #return '{"del_msg0":"参数"}path=%s, filename=%s;' %(fpath, fname);
    
    print("2. can_not_delete_on_web_dirs:", can_not_delete_on_web_dirs, len(can_not_delete_on_web_dirs) )

    fnameArr=re.split('\,', fnameStr) #分为数组
    rs=""
    for fname in fnameArr:
        print("1. fname:", fname)
        fpathT = os.path.abspath( os.path.join(root_path, fpath) ) #真实路径
        if os.path.isfile(os.path.join(fpathT,fname)): # and os.path.isdir(fpath):
            curFile=os.path.join(fpathT, fname);
            
            # A2. 是否在 禁止删除的文件夹中
            for can_not_delete_on_web_dir in can_not_delete_on_web_dirs:
                outer_dir=os.path.abspath( os.path.join(root_path, can_not_delete_on_web_dir) )
                if is_folder_contained(fpathT, outer_dir):
                    return jsonify({"msg": "该文件夹下不能删除，见 config.ini [system]can_not_delete_on_web", "path": fpath, 'filename': fpath});
                # A3 如果是文件，则不允许删除
                if curFile==outer_dir:
                    print("warning: can NOT delete '", curFile, "' as it sits in config.ini::can_not_delete_on_web!")
                    return jsonify({"msg": "该文件不能删除，见 config.ini [system]can_not_delete_on_web", "path": fpath, 'filename': can_not_delete_on_web_dir});
            
            #如果不存在垃圾箱，则新建垃圾箱
            dustbin=os.path.abspath( os.path.join(root_path, "dustbin") );
            if not os.path.exists(dustbin):
                os.mkdir(dustbin)
            #如果不在垃圾箱，则移动到垃圾箱
            #rs+="; dustbin="+dustbin +"; fpath="+fpathT+'; ';
            if(dustbin!=fpathT):
                shutil.move(curFile, os.path.join(dustbin,fname) );
                rs+=fname+' moved to /dustbin/, ';
            else:
                os.remove(curFile);
                rs+=fname+' removed, ';
        else:
            return jsonify({"msg": "参数不正确", "path": fpath, 'filename': fname});
    return jsonify({'msg': "deleted!", 'filename': rs});
#





# get方法：查询当前路径下的所有文件
# v0.2 对文件按时间倒序排序
@server.route('/list', methods=['get'])
@login_required  # 使用装饰器
def get_file_list():
    debug='';
    fpath = request.values.get('fpath', './') #获取用户输入的目录

    # 空地址自动回首页
    if fpath=="":
        fpath="./"
    if fpath[-1]!="/":
        fpath+="/";
    
    #debug+=fpath;
    
    #保护目录: 保证只能传入相对路径
    if not fpath.startswith("."):
        if fpath.startswith("/"):
            fpath='.'+fpath;
        else:
            fpath='./'+fpath;
    #
    #如果路径出现../开头或者路径中出现/../字样，报非法，返回首页。
    if fpath.startswith("../") or (re.search("\/\.\.\/", fpath)!=None):
        return "<a href='/list'>Go Home</a> <br>Invalid '..' detected in fpath, please use valid path! <br>"+fpath;
    #
    fpathT=os.path.join(root_path, fpath); #真实地址
    debug+="<div id=fpath style='display:none;'>"+fpath+"</div>";
    #生成顶部路径超链接
    #str to arr, by the end of path
    sep=fpathT[-1];
    arr=re.split(sep, fpath);
    #arr to a links
    url="/list?fpath="
    urlPath="<a id=root href=/list>/RootDir</a>/";    
    for i in range(len(arr)-1 ):
        if arr[i]=='.':
            continue;
        if i<len(arr)-2:
            url=url+ arr[i]+"/";
            urlPath+="<a href="+url+">"+arr[i]+"</a>/"
        else:
            urlPath+=arr[i]+"/";
        #print(i, arr[i], urlPath)
    topPath="<h4 class=root><span><a id='delete' class=button href='javascript:void(0);'>Delete</a></span> Index of "+urlPath+"</h4>\n\n"; #cut to pieces.

    #返回上一级的url链接和tr
    htmlBack="";
    if fpath!="./":
        arr=re.split('/',fpath);
        urlBack="/".join(arr[:-2])+"/";
        htmlBack="<tr class=header><td></td> <td>"+img['back']+" <a title='点击返回上一级' href='/list?fpath="+urlBack+"'>..</a>"+"</td>  <td></td>  <td></td> <tr>\n"
    #tr 文件和文件夹
    htmlF="";
    htmlD="";
    table1="<div class=wrap><fieldset> <legend>File List</legend> "+topPath+"\
<table><tr class=header> <th></th> "+\
"<th>"+img['order']+"FileName</th>   <th>"+img['order']+"Size</th>   <th>"+img['order']+"Modified</th> </tr>\n"
    if os.path.isdir(fpathT):
        filelist = os.listdir(fpathT)
        #为文件增加时间2列，类型列，大小,url
        filelist2d=[]
        for i in range(len(filelist)):
            file=filelist[i];
            url=os.path.join(fpath, file); #显示用虚拟文件路径
            urlT=os.path.join(fpathT, file); #获取都要用真实路径
            arrTime=getModifiedTime(urlT);#真实路径获取时间 ['2019-04-17 09:13:35', 1555463615.2421255]
            #
            type=''
            if os.path.isfile(urlT):
                type='file'
                Size=getDocSize(urlT);
            elif os.path.isdir(urlT):
                type='dir'
                Size=formatSize(dirSize(urlT));#递归计算文件夹大小
            else:
                type="file"
                Size="Not Available"
            #合并
            filelist2d.append([file, arrTime[0],arrTime[1], type, Size, url])
        filelist2d.sort(key=lambda x:-x[2]) #按时间降序
        #print('3===>', filelist2d,'\n')
        
        for i in range(len(filelist2d)):
            arr=filelist2d[i]
            #['tmp.R', '2019-10-25 13:28:17', 1571981297.2041583, 'file', '17.36kb', './tmp.R'],
            file=arr[0]; #文件名，如果过长，尝试截断显示：优先用js实现
            
            #fix: 在url中的文件名不能含有+号，要替换几个关键字符
            fileInURL=re.sub("\%", "%25", file);
            fileInURL=re.sub("\+", "%2B", fileInURL); 
            fileInURL=re.sub("\&", "%26", fileInURL);
            fileInURL=re.sub("\#", "%23", fileInURL);
            fileInURL=re.sub("\=", "%3D", fileInURL);

            #fileInURL=re.sub("－", "%E2%80%90", fileInURL);
            fileInURL=re.sub(" ", "%20", fileInURL);
            
            fTime=arr[1];
            fTimeNum=arr[2];
            fType=arr[3]
            Size=arr[4]
            url=arr[5]
            #
            if fType=='file': #type="file"
                # 外链地址
                urlPath_out="<a href='/file/"+fpath+fileInURL+"'><i class='fa fa-external-link' style='margin-left:10px;' title='外链 - 请右击复制'></i></a>";
                
                # 文件的后缀名
                arr=re.split('\.', file)
                suffix=arr[len(arr)-1].lower()
                
                
                # setting icon by suffix
                if suffix in ['png', 'jpg', 'jpeg', 'bmp', 'gif','svg']:
                    icon=img['png']
                elif suffix in ['pdf', 'PDF']:
                    icon=img['pdf']
                elif suffix in ['html', "htm"]:
                    icon=img['html']
                elif suffix in ['mp3', "wav", "m4a"]:
                    icon=img['audio']
                elif suffix=='mp4':
                    icon=img['mp4']
                elif suffix in ['txt', 'md', 'lrc', 'vcf']:
                    icon=img['txt']
                
                elif suffix in [ 'ipynb']:
                    icon=img['note']
                elif suffix in [ 'py']:
                    icon=img['py']
                elif suffix in ["r", "rmd"]:
                    icon=img['R']
                elif suffix in [ 'php']:
                    icon=img['php']
                elif suffix in ["php", "pl", "sh", 'c', 'cpp']:
                    icon=img['code']

                elif suffix in ["rar", "zip", "gz"]:
                    icon=img['rar']
                
                elif suffix in ["doc", "docx"]:
                    icon=img["doc"]
                elif suffix in ["ppt", "pptx"]:
                    icon=img["ppt"]
                elif suffix in ["xls", "xlsx", "csv"]:
                    icon=img["xls"]

                elif suffix in ["conf", "css", "yaml", "yml"]:
                    icon=img["conf"]
                elif suffix in ["js"]:
                    icon=img["js"]
                elif suffix in ["bat"]:
                    icon=img["bat"]

                else:
                    icon=img['file']
                #

                #1. 预览图标与地址, suffix in lower case
                picViewPath=""

                # 普通预览：图片
                if suffix in ['png', 'jpg','jpeg','bmp', 'gif', 'svg', 'pdf', 'html', 'mp3', 'mp4']:
                    picViewPath="<a target=_blank href='/show/?url=/file/"+fpath+fileInURL+"'><i class='fa fa-picture-o' style='margin-left:10px;' title='预览 - 单击预览'></i></a>";
                    #
                    picViewPath=picViewPath.replace("/show/?url=", "")
                    if suffix=="pdf":
                        picViewPath=picViewPath.replace(".pdf", ".pdf#toolbar=0")
                # 预览文本: plain text
                elif suffix in ["txt", "css", "js"]:
                    picViewPath="<a target=_blank href='/file/"+fpath+fileInURL+"'><i class='fa fa-picture-o' style='margin-left:10px;' title='预览 - 单击预览'></i></a>";
                #预览 markdown
                elif suffix in ["md", "markdown", "rmd"]:
                    picViewPath="<a target=_blank href='/md_reader/?file=/file/"+fpath+fileInURL+"'><i class='fa fa-picture-o' style='margin-left:10px;' title='预览 - 单击预览'></i></a>";

                #2. 预览代码 code: 适合纯文本文件
                codeViewPath=""
                if suffix in ['py', 'r', "c", "cpp", "c++", "h", "sh", "js", "pl", "rmd", "php", "md", "gitignore", "ini", "config", "csv", \
                              "lrc", "bat", "log", \
                              # for output of featureCounts and STAR
                              "summary", "out", \
                              #for SNP output
                              "vcf", "pileup", "bed",
                              "smk", "yaml"]:
                    codeViewPath="<a target=_blank href='/code_reader/?file=/file/"+fpath+fileInURL+"'><i class='fa fa-code' style='margin-left:10px;' title='预览代码 - 单击预览'></i></a>";
                # 预览 jupyter notebook
                elif suffix in ["ipynb"]:
                    codeViewPath="<a target=_blank href='/nbpreview/?filename=/file/"+fpath+fileInURL+"'><i class='fa fa-code' style='margin-left:10px;' title='预览代码 - 单击预览'></i></a>";

                #3. 播放音频图标 music
                audioPlayPath=""
                if suffix in ['mp3', 'wav', 'm4a']:
                     audioPlayPath="<a target=_blank href='"+playerPath+ \
                     request.host_url + \
                     "file/"+strip(fpath, "^\./")+fileInURL+"'><i class='fa fa-play-circle-o' style='margin-left:10px;' title='播放 - 单击新标签页播放'></i></a>";
                htmlF+="<tr class=file data-time='"+str(fTimeNum)+"'> <td><input type='checkbox' tabindex='-1'></td> <td>"+icon+ \
                    " <a title='点击下载' target='_blank' href='/download?filename="+fileInURL+"&path="+fpath+"'>"+file+'</a> '+urlPath_out + \
                        clean_url(picViewPath) + \
                        clean_url(codeViewPath) + \
                        clean_url(audioPlayPath) + \
                        "</td>  <td>"+Size+"</td>   <td>"+fTime+"</td>  </tr>\n"
            elif fType=='dir': #type="dir"
                htmlD+="<tr class=dir data-time='"+str(fTimeNum)+"'> <td><input type='checkbox' tabindex='-1'></td> <td>"+img['dir']+" <a title='点击打开' href='/list?fpath="+url+"/'>"+file+"/</a></td> <td>"+Size+"</td>  <td>"+fTime+"</td>  </tr>\n"
    #files = [file for file in filelist if os.path.isfile(os.path.join(fpath, file))]
    return head+debug+table1+htmlBack+htmlF+htmlD+"</table> </fieldset></div>"+foot









# post方法：上传文件的 https://www.cnblogs.com/wl443587/p/10552542.html
@server.route('/upload', methods=['POST'])
@login_required
def upload():
    # 如果不允许上传文件，则直接返回错误提示
    if not allow_file_upload:
        return '{"msg": "<b style=\'color:red;\'>Error: uploading file is not allowed now!</b>\
            Please ask the administrator for help: setting allow_file_upload=true in config.ini<br> \
            Click <b>back arrow</b> on your web brower to go back."}'

    fname = request.files.get('file')  #获取上传的文件
    uploadDir = request.form['uploadDir'] #获取上传的路径
    uploadDir = request.form.get('uploadDir','./') #获取上传的路径
    #return uploadDir;

    ip = request.remote_addr

    if fname:
        url=url_for('get_file_list',fpath=uploadDir );
        
        # fix: 禁止文件名包含&，否则会导致下载失败
        #if re.search('&', fname.filename) != None:
        #    #return '{"msg": "文件名不能包含特殊符号 & (请去掉该符号后重新上传)"}'
        #    return '<meta http-equiv="refresh" content="3;url='+url+'">'+"<span style='color:red;'>Failed!</span> <br><b style='font-size:30px;'>delete & in your filename and upload again.</b> <br>Returning to list in 5 seconds.<br>";
        
        t = time.strftime('%Y%m%d%H%M%S')
        new_fname = os.path.join(root_path, uploadDir, fname.filename);
        if os.path.exists(new_fname): #如果存在，则加时间戳前缀
            new_fname = os.path.join(root_path, uploadDir, t +'_'+ fname.filename);
        fname.save(new_fname)  #保存文件到指定路径
        
        #logger
        if not os.path.exists('dustbin'):
            os.mkdir('dustbin')
        with open("dustbin/upload.logger.txt", 'a', encoding='utf-8') as f:
            timsString=time.strftime("%Y/%m/%d-%H:%M:%S", time.localtime())
            f.write('\t'.join([timsString, ip, fname.filename, new_fname])+"\n" )
        
        #return url;
        return '<meta http-equiv="refresh" content="3;url='+url+'">'+"Upload Success!. Returning to list in 3 seconds.<br>";
    else:
        return '{"msg": "上传文件不能为空！(请先选择上传文件)"}'



# 添加新静态文件的路径，这样就允许xx/下的图片加载了
@server.route("/static/<path:filename>")
def static_src(filename):
    return send_from_directory("static",filename,as_attachment=False)

# 添加新静态文件的路径，这样就允许 nb/下的css/js资源加载了
@server.route("/nbpreview/<path:filename>")
def nbpreview_src(filename):
    return send_from_directory("templates/nbpreview/",filename,as_attachment=False)

# 添加新静态文件的路径，这样就允许 md/下的css/js资源加载了
@server.route("/md_reader/<path:filename>")
def md_reader_src(filename):
    return send_from_directory("templates/md_reader/",filename,as_attachment=False)

# /favicon.ico
@server.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(server.root_path, 'static/images/'),
                               'favicon.ico', mimetype='image/vnd.microsoft.icon')



# 几个在linux终端打印彩色文字的函数
def print_yellow(str):
    print( f"\033[40;33m{str}\033[0m" )
def print_red(str):
    print( f"\033[40;31m{str}\033[0m" )

# for win cmd
# https://www.delftstack.com/howto/python/python-print-colored-text/
# https://github.com/tartley/colorama
def print2(color, str):
    if env=="linux":
        if color=="red":
            print_red(str)
        else:
            print_yellow(str)
    elif env=="win32":
        import colorama
        from colorama import Fore
        from colorama import Style
        colorama.init()
        if color=="red":
            print(Fore.RED + Style.BRIGHT + str + Style.RESET_ALL)
        else:
            print(Fore.YELLOW + Style.BRIGHT + str + Style.RESET_ALL)
    else:
        print(str);


# run the app
if __name__ == '__main__':
    server.debug = True # 设置调试模式，生产模式的时候要关掉debug

    # 显示欢迎屏
    print2("yellow", "#"*45);
    print2("yellow", "#    Welcome to webPan.py                   #");
    print2("yellow", f"#       * Version: {version}                   #" )
    print2("yellow", f"#       * Environment: {env}                #" )
    print2("yellow", f"#       * root_path: {root_path}" )
    print2("yellow", f"#       * login: user:{user}, passwd:{passwd}" )
    print2("yellow", f"#       * allow_file_upload: {allow_file_upload}           #" )
    print2("yellow", f"#       * can_not_delete_on_web_dirs: {can_not_delete_on_web_dirs}" )
    print2("yellow", f"#       * dirs_calc_size: {dirs_calc_size}" )
    print2("red", f"#       * URL: http://127.0.0.1:{port}        #" )
    print2("yellow", f"#    https://github.com/BioMooc/webPan.py   #" )
    print2("yellow", "#"*45);

    server.run(host="0.0.0.0",port=port)
#